Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add abstract eloquent casts #526

Merged
merged 4 commits into from
Aug 9, 2023
Merged

Add abstract eloquent casts #526

merged 4 commits into from
Aug 9, 2023

Conversation

rubenvanassche
Copy link
Member

Sometimes you have an abstract parent data object with multiple child data objects, for example:

abstract class RecordConfig extends Data
{
    public function __construct(
        public int $tracks,
    ) {}
}

class CdRecordConfig extends RecordConfig
{
    public function __construct(
        int $tracks
        public int $bytes,
    ) {
        parent::__construct($tracks);
    }
}

class VinylRecord extends RecordConfig
{
    public function __construct(
        int $tracks
        public int $rpm,
    ) {
        parent::__construct($tracks);
    }
}

A model can have a JSON field which is either one of these data objects:

class Record extends Model
{
    protected $casts = [
        'config' => RecordConfig::class,
    ];
}

You can then store either a CdRecordConfig or a VinylRecord in the config field:

$cdRecord = Record::create([
    'config' => new CdRecordConfig(tracks: 12, bytes: 1000),
]);

$vinylRecord = Record::create([
    'config' => new VinylRecord(tracks: 12, rpm: 33),
]);

$cdRecord->config; // CdRecordConfig object
$vinylRecord->config; // VinylRecord object

When a data object class is abstract and used as an Eloquent cast then this feature will work out of the box.

The child data object value of the model will be stored in the database as a JSON string with the class name as the key:

{
    "type": "\\App\\Data\\CdRecordConfig",
    "value": {
        "tracks": 12,
        "bytes": 1000
    }
}

When retrieving the model, the data object will be instantiated based on the type key in the JSON string.

Abstract data class morphs

By default, the type key in the JSON string will be the fully qualified class name of the child data object. This can break your application quite easily when you refactor your code. To prevent this, you can add a morph map like with Eloquent models. Within your AppServiceProvivder you can add the following mapping:

use Spatie\LaravelData\Support\DataConfig;

app(DataConfig::class)->enforceMorphMap([
    'cd_record_config' => CdRecordConfig::class,
    'vinyl_record_config' => VinylRecordConfig::class,
]);

Why no DataCollection support?

At this point in type a DataCollection can only be of one type only, so no use to make a cast for abstract child classes for now.

@rubenvanassche rubenvanassche merged commit 0de2ecf into main Aug 9, 2023
20 checks passed
@rubenvanassche rubenvanassche deleted the abstract-eloquent-casts branch August 9, 2023 14:09
@bentleyo
Copy link
Contributor

@rubenvanassche this is really cool, I like the approach you took!

Is the plan to extend this functionality in the future so it can be used with collections etc.?

Just checking as we're currently using a trait to handle some dynamic data class functionality and it would be great if there was built-in support for this in the package!

@rubenvanassche
Copy link
Member Author

I really want to implement such a thing, but am not sure at the moment how we're going to do it. Especially since validation of such collections for example will not work due to the different types.

This was really a simple fix for a problem we we're having in one of ours projects.

But as soon as v4 is out I'll start taking a look at these types of collections.

@bentleyo
Copy link
Contributor

Thanks for replying @rubenvanassche! In our situation it was fine for us to have a property on the abstract class that identified which concrete version of the class to use (and that's the approach we took with the trait I linked above), but I can see how that might not be a solution everyone would be happy with.

In some ways I feel like having a piece of data on the abstract data class and having it determine which concrete version of the class to validate against / instantiate makes sense. Without the identifying data in the abstract class it was really awkward to work with because we'd always need to pass in the type / class name of the data object to use externally - especially awkward when working with collections of the same abstract type.

There's probably better ways to solve that issue, but for us it felt like a reasonable trade-off requiring some piece of variant-identifying data in the objects themselves.

Cheers mate!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants